Airbnb-hintaennustin

Johdatus datatieteeseen 2019 syksy, harjoitustyö, Tapio Vaaranmaa (54338)

1. Kehitysympäristö

Hintaennustin tehtiin Python-koodilla Jupyter Notebooks -työkalun avulla. Myös dokumentaation kirjoitin suoraan Jupyter Notebook -tiedostoon Markdown-soluihin. Kehitysympäristöksi asensin omaan kannettavaan tietokoneeseeni Jupyter Notebooksin sisältävän Anaconda-alustan (2019-10, Anaconda Navigator 1.9.7). Anaconda on erityisesti datatieteeseen ja koneoppiseen soveltuva ja runsaasti käytetty avoin Python- ja R-alusta, jota on helppo laajentaa Conda-paketinhallintatyökalulla. Anaconda Navigator on Anaconda-alustan graafinen käyttöliittymä, jolla voi luoda ja hallita erillisiä ympäristöjä sekä käynnistää valittuun ympäristöön asennettuja sovelluksia. Jupyter Notebook on selaimella käytettävä web-sovellus. Sitä käytetään aina selaimesta siitä riippumatta, onko sen palvelin paikallisella tietokoneella vai verkon takana.

Käyttöjärjestelmänä on Ubuntu 18.04 Linux, ja harjoitustyön julkaisualustana on GitHub-repositorio. Editorina käytin Visual Studio Codea ja vanhaa kunnon Emacsia, joskaan niitä ei juurikaan tarvinnut, sillä lähes kaiken sai kätevästi tehtyä suoraan Jupyter Notebookilla. Asentamani Anaconda-distribuution Pythonin versio on 3.7.4 ja Jupyter Notebookin versio on 6.0.1.

Asennuksessa tuli vähän sekoiltua, kun asensin ensin Pythonin uuden version suoraan ja sain koko käyttöjärjestelmän sekaisin määrittelemällä tämän uuden Python-tulkin oletustulkiksi: terminaaliohjelmakaan ei enää käynnistynyt. Tämä johtui siitä, että Linux-käyttöjärjestelmässä käynnistetään kaikenlaista Pythonin avulla ja tämä koodi on vanhempaa Pythonia, joten kaikki ei enää käynnistynyt käyttöjärjestelmässä kuten pitäisi. Tämä asennus oli kaiken lisäksi ihan turha, sillä Anacondan mukana tulee tarvittava Python-ympäristökin. Poistin tuhan asennuksen ja asensin Anacondan ohjeiden mukaisesti. Linuxiin asentaminen ei ole ihan niin suoraviivaista kuin Windowsiin, mutta netistä löytyneiden ohjeiden avulla se sujui kuitenkin kohtuullisen kivuttomasti.

Toinen varteenotettava vaihtoehto olisi ollut käyttää Jypyter Notebook -sovellusta CSC Notebooks -pilvipalvelun aikarajoitetutussa virtuaalikoneessa kuten koodiklinikalla tehtiin. Päädyin kuitenkin asentamaan tarvittavat ohjelmat omaan kannettavaan tietokoneeseeni, sillä sitä olisin kuitenkin käyttänyt, vaikka sovellusta olisikin ajettu verkon yli. Tämän vuoksi helpointa oli tehdä kaikki suoraan paikallisesti omalla koneella, ja näin ympäristökin jää minulle talteen.

2. Datan kerääminen ja tarkastelu

Päätin käyttää Inside Airbnb -datasettiä. Olisi ollut mielenkiintoisempaa tutkia jotain muuta data-aineistoa, mutta kurssi oli pakko jo saada pakettiin mahdollisimman pian ja siksi päätin valita tämän valmiin ja harjoituksista tutun datasetin. Tutkittavaksi kaupungiksi valitsin Wienin, sillä kaikki minulle tutummat kaupungit oli jo valittu tutkittavaksi. Minulle tutummista Budapestistä ja Etelä-Ranskan kaupungeista ei ollut data-aineistoa tässä datasetissä, enkä halunnut valita myöskään harjoituksissa käsiteltyä Berliiniä. Wien oli ainoa käsittelemätön kaupunki, josta minulla oli ennestään edes joku käsitys.

Inside Airbnb -datasetissä on Wienistä tallennettuna tiedostot listings.csv.gz, calendar.csv.gz, reviews.csv.gz, listings.csv, reviews.csv, neighbourhoods.csv ja neighbourhoods.geojson. Näistä ensimmäinen on mielenkiintoisin sisältäen yksityiskohtaista tietoa varauskohteista, toinen sisältää yksityiskohtaisen varauskalenterin ja kolmas kohteiden tekstimuotoisia arviointeja. Kaksi pakkaamatonta tiedostoa ovat vastaavien pakattujen tiedostojen yhteenvetoja ja viimeinen tiedosto sisältää tiedostossa neighbourhoods.csv lueteltujen kaupunginosien paikkatietoja. Dataa oli yli 10 tuhannesta kohteesta, joten sitä oli riittävästi, ja tiedot oli päivitetty viimeksi 19.11. 2019, joten data lienee ajantasaista ja luotettavaa.

Harjoitustyön pohjana käytin Nick Amaton blogia Airbnb price predictor. Ihan ensimmäisenä otetaan käyttöön datan keräämiseen, jalostamiseen, kuvailemiseen sekä koneoppimisessa tarvittavia kirjastoja. Tutustuin aluksi tiedostojen tiedostot listings.csv.gz, calendar.csv.gz, reviews.csv.gz sisältöihin lukemalla ne Pandas-kirjaston dataframeiksi. Listings-dataframe sisälsi yhteensä 106 eri saraketta kohdeindeksi mukaan lukien. Reviews-dataframessa ainoa varsinainen tietosarake sisälsi tekstimuotoisia arvosteluja, joiden hyödyntäminen olisi ollut varsin vaikeaa tässä työssä. En myöskään kokenut tarpeelliseksi käyttää Calendar-dataframen tietoja hintaennustimessa, sillä sen sisältämät tulevaisuuden varaustiedot ja hintapyynnöt eivät tuntuneet ennusteen kannalta relevanteilta selittäjiltä. Tämän vuoksi päätin käyttää ennustimessa vain listings-tietoja ja pitäytyä alkuperäisen esimerkin sarakevalinnoissa, mutta karttavisualisointien vuoksi otin mukaan myös sarakkeet 'latitude' ja 'longitude' sekä kohteen Airbnb-linkin, jonka avulla valittua kohdetta voi tarkastella yksityiskohtaisesti. Lisäksi otin mukaan avainkentän ’id’ siltä varalta, että haluaisin kuitenkin myöhemmin hyödyntää calendar- tai reviews-tietoja. Otin talteen myös aineistossa olevan geojson-tiedoston wep-osoitteen, sillä käytän sitä karttavisualisoinneissa.

Halusin harjoitustyön lopuksi tehdä jonkinlaisen vuorovaikutteisen dashboardin Power BI -ohjelmistolla, mutta sen karttavisualisointi ei löytänyt kaikkia kaupunginosia pelkän nimen perusteella. Tämä johtunee kaupunginosien nimien sisältämistä umlaut- ja ß-merkeistä, jotka muutenkin aiheuttivat harmia pitkin harjoitustyötä. Wienissä käytetään yleisemmin kaupunginosanumeroita kuin niiden nimiä (esimerkiksi Innere Stadt on 1. Bezirke). Power BI -ohjelman karttavisualisointi löysikin kaupunginosat tämän kaupunginosanumeron perusteella. Tämän vuoksi lisäsin kaupunginosanumerot kohteet sisältävään dataframeen. Tein tämän raapimalla kaupunginosanumerot ja -nimet Wienin kaupungin web-sivuilta. Raavituista tiedoista muodostin ensin oman dataframen ja lopuksi yhdistin dataframet yhteen Pandaksen join-operaatiolla. Näin tuli kokeiltua oman datan keräämistäkin joskin varsin minimaalisella esimerkillä.

In [1]:
import pandas as pd
import requests
from bs4 import BeautifulSoup
import numpy as np
import folium                              # conda install folium -c conda-forge
from folium.plugins import HeatMap 
from folium.plugins import MarkerCluster
import urllib
import json
from sklearn import impute
from sklearn import ensemble
from sklearn import linear_model
from sklearn.model_selection import learning_curve,GridSearchCV
from sklearn import preprocessing
from sklearn.model_selection import train_test_split
import sklearn.metrics as metrics
import matplotlib.pyplot as plt
from collections import Counter
In [2]:
folder = 'http://data.insideairbnb.com/austria/vienna/vienna/2019-11-19/data/'
listings_file = folder + 'listings.csv.gz'
calendar_file = folder + 'calendar.csv.gz'
reviews_file  = folder + 'reviews.csv.gz'

folder = 'http://data.insideairbnb.com/austria/vienna/vienna/2019-11-19/visualisations/'
geojson_file  = folder + 'neighbourhoods.geojson'
In [3]:
df_c = pd.read_csv(calendar_file, compression='gzip')
In [4]:
df_c.head()
Out[4]:
listing_id date available price adjusted_price minimum_nights maximum_nights
0 15883 2019-11-20 f $106.00 $106.00 1 999
1 532596 2019-11-20 f $160.00 $160.00 28 100
2 532596 2019-11-21 f $160.00 $160.00 28 100
3 532596 2019-11-22 t $160.00 $160.00 28 100
4 532596 2019-11-23 t $160.00 $160.00 28 100
In [5]:
df_r = pd.read_csv(reviews_file, compression='gzip')
In [6]:
df_r.head(2)
Out[6]:
listing_id id date reviewer_id reviewer_name comments
0 15883 29643839 2015-04-10 30537860 Robert If you need a clean, comfortable place to stay...
1 15883 80590019 2016-06-19 37529754 Chuang It's so nice to be in the house! It's a peace ...
In [7]:
df = pd.read_csv(listings_file, compression='gzip')
In [8]:
print(df.columns[0:50])   # tai print(pd.Series(df.columns).head(50))
print(df.columns[50:106])
len(df)
Index(['id', 'listing_url', 'scrape_id', 'last_scraped', 'name', 'summary',
       'space', 'description', 'experiences_offered', 'neighborhood_overview',
       'notes', 'transit', 'access', 'interaction', 'house_rules',
       'thumbnail_url', 'medium_url', 'picture_url', 'xl_picture_url',
       'host_id', 'host_url', 'host_name', 'host_since', 'host_location',
       'host_about', 'host_response_time', 'host_response_rate',
       'host_acceptance_rate', 'host_is_superhost', 'host_thumbnail_url',
       'host_picture_url', 'host_neighbourhood', 'host_listings_count',
       'host_total_listings_count', 'host_verifications',
       'host_has_profile_pic', 'host_identity_verified', 'street',
       'neighbourhood', 'neighbourhood_cleansed',
       'neighbourhood_group_cleansed', 'city', 'state', 'zipcode', 'market',
       'smart_location', 'country_code', 'country', 'latitude', 'longitude'],
      dtype='object')
Index(['is_location_exact', 'property_type', 'room_type', 'accommodates',
       'bathrooms', 'bedrooms', 'beds', 'bed_type', 'amenities', 'square_feet',
       'price', 'weekly_price', 'monthly_price', 'security_deposit',
       'cleaning_fee', 'guests_included', 'extra_people', 'minimum_nights',
       'maximum_nights', 'minimum_minimum_nights', 'maximum_minimum_nights',
       'minimum_maximum_nights', 'maximum_maximum_nights',
       'minimum_nights_avg_ntm', 'maximum_nights_avg_ntm', 'calendar_updated',
       'has_availability', 'availability_30', 'availability_60',
       'availability_90', 'availability_365', 'calendar_last_scraped',
       'number_of_reviews', 'number_of_reviews_ltm', 'first_review',
       'last_review', 'review_scores_rating', 'review_scores_accuracy',
       'review_scores_cleanliness', 'review_scores_checkin',
       'review_scores_communication', 'review_scores_location',
       'review_scores_value', 'requires_license', 'license',
       'jurisdiction_names', 'instant_bookable', 'is_business_travel_ready',
       'cancellation_policy', 'require_guest_profile_picture',
       'require_guest_phone_verification', 'calculated_host_listings_count',
       'calculated_host_listings_count_entire_homes',
       'calculated_host_listings_count_private_rooms',
       'calculated_host_listings_count_shared_rooms', 'reviews_per_month'],
      dtype='object')
Out[8]:
12615
In [9]:
cols = ['id',
        'price',
        'accommodates',
        'bedrooms',
        'beds',
        'neighbourhood_cleansed',
        'room_type',
        'cancellation_policy',
        'instant_bookable',
        'reviews_per_month',
        'number_of_reviews',
        'availability_30',
        'review_scores_rating',
        'latitude',
        'longitude',
        'listing_url'
        ]

# read the file into a dataframe
df = pd.read_csv(listings_file, compression='gzip', encoding='utf-8', usecols=cols)
In [10]:
print(df.head())
      id                         listing_url neighbourhood_cleansed  latitude  \
0  15883  https://www.airbnb.com/rooms/15883             Donaustadt  48.24144   
1  38768  https://www.airbnb.com/rooms/38768           Leopoldstadt  48.21823   
2  40625  https://www.airbnb.com/rooms/40625   Rudolfsheim-FŸnfhaus  48.18486   
3  51287  https://www.airbnb.com/rooms/51287           Leopoldstadt  48.21851   
4  70568  https://www.airbnb.com/rooms/70568             Donaustadt  48.22224   

   longitude        room_type  accommodates  bedrooms  beds    price  \
0   16.42812       Hotel room             3       1.0   1.0   $85.00   
1   16.37926  Entire home/apt             5       1.0   3.0   $65.00   
2   16.32740  Entire home/apt             6       2.0   4.0  $130.00   
3   16.37781  Entire home/apt             3       0.0   2.0   $60.00   
4   16.42460  Entire home/apt             2       1.0   1.0   $59.00   

   availability_30  number_of_reviews  review_scores_rating instant_bookable  \
0               21                 10                  96.0                t   
1               10                303                  95.0                f   
2               19                149                  97.0                t   
3                9                281                  92.0                f   
4               30                 10                  94.0                f   

           cancellation_policy  reviews_per_month  
0                     moderate               0.18  
1  strict_14_with_grace_period               2.87  
2                     moderate               1.32  
3  strict_14_with_grace_period               2.62  
4  strict_14_with_grace_period               0.10  

Tarkastellessani dataframen sisältöä, huomasin, että osa sarakkeen ’ neighbourhood_cleansed’ merkeistä jää tulostumatta oikein. Tämän ongelman selvittämiseksi ja korjaamiseksi piti tehdä vähän lisätarkasteluja.

In [11]:
df.neighbourhood_cleansed.unique()
Out[11]:
array(['Donaustadt', 'Leopoldstadt', 'Rudolfsheim-F\x9fnfhaus',
       'Ottakring', 'Brigittenau', 'Neubau', 'Margareten', 'Hernals',
       'Floridsdorf', 'Simmering', 'Wieden', 'Alsergrund', 'Innere Stadt',
       'Mariahilf', 'Meidling', 'Josefstadt', 'Landstra§e', 'Favoriten',
       'W\x8ahring', 'Penzing', 'D\x9abling', 'Liesing', 'Hietzing'],
      dtype=object)
In [12]:
df1 = pd.read_csv(listings_file, compression='gzip', encoding='utf-8', 
                  usecols=['neighbourhood'])
df1.neighbourhood.unique()
Out[12]:
array(['Donaustadt', 'Leopoldstadt', 'Rudolfsheim-Fünfhaus', 'Ottakring',
       'Brigittenau', 'Neubau', 'Margareten', 'Hernals', 'Floridsdorf',
       'Simmering', 'Wieden', 'Alsergrund', 'Innere Stadt', 'Mariahilf',
       'Meidling', 'Josefstadt', 'Landstraße', 'Favoriten', 'Währing',
       'Penzing', 'Döbling', 'Liesing', 'Heitzing', nan], dtype=object)

Tarkasteltuani muiden sarakkeiden sisältöjä huomasin, että sarakkeen ’ neighbourhood_cleansed’ merkit on koodattu eri tavalla kuin muiden sarakkeiden merkit. Muualla koodaus oli utf-8, mutta sarakkeessa ’ neighbourhood_cleansed’ oli varsin epämääräinen koodaus. Koodaus vaikutti olevan muuten utf-8, mutta umlaut-merkien ja ß -merkin jälkimmäinen tavu näytti olevan mac_roman-koodauksen vastaava tavukoodi. Korjasin nämä virheelliset merkit metodin replace avulla, minkä jälkeen sarakkeen ’neighbourhood_cleansed’ näytti taas järkevältä ja kaupunginosien nimet tulostuivat siinä oikein.

Tämän asian ymmärtämiseen kului käsittämättömän paljon aikaa, sillä ensin luulin koko tiedoston merkkikoodauksen olevan joku erikoisempi, mutta mitään sopivaa ei löytynyt. Selitys löytyi vasta, kun aloin tutkia merkkijonojen tavusisältöä. Sitäkään tehdessä ei ensimmäisenä tule mieleen, että koodaus mennyt sekaisin, mutta mitään muuta selitystä en keksinyt. Muidenkin saksakielisten kaupunkien tiedostoja katselin enkä löytänyt niistä samaa ongelmaa.

In [13]:
df['neighbourhood_cleansed'] = \
    df['neighbourhood_cleansed'].replace(['\u008A','\u009A','\u009F','\u0080',
                                          '\u0085','\u0086','\u00A7'],
                                         ['ä','ö','ü','Ä','Ö','Ü','ß'], regex=True)
df.neighbourhood_cleansed.unique()
Out[13]:
array(['Donaustadt', 'Leopoldstadt', 'Rudolfsheim-Fünfhaus', 'Ottakring',
       'Brigittenau', 'Neubau', 'Margareten', 'Hernals', 'Floridsdorf',
       'Simmering', 'Wieden', 'Alsergrund', 'Innere Stadt', 'Mariahilf',
       'Meidling', 'Josefstadt', 'Landstraße', 'Favoriten', 'Währing',
       'Penzing', 'Döbling', 'Liesing', 'Hietzing'], dtype=object)

Sitten raavitaan kaupunginosanumerot Wienin kaupungin web-sivuilta ja lisätään ne dataframeen, jotta voidaan lopuksi tehdä karttavisualisointi Power BI:lla.

In [14]:
response = requests.get('https://www.vienna.at/features/bezirke-wien') # read the web page
soup = BeautifulSoup(response.content, 'html.parser')

# Convert to string
pretty = soup.prettify()
#print(pretty)
In [15]:
bezirkes = soup.select('ul[id=bezirke] li', limit = 23)     # Soup object type, 
                                                            # total 23 bezirkes in Vienna
ordernumbers = []
names        = []
addresses    = []
In [16]:
# Go through all bezirkes in the list
for bezirke in bezirkes:
    # Get bezirke order number, name and address
    bezirkeData = bezirke.text.split('\n')
    ordernumbers.append(bezirkeData[1])
    names.append(bezirkeData[2])
    addresses.append(bezirkeData[3])
    
print(ordernumbers, names, addresses)
['1.', '2.', '3.', '4.', '5.', '6.', '7.', '8.', '9.', '10.', '11.', '12.', '13.', '14.', '15.', '16.', '17.', '18.', '19.', '20.', '21.', '22.', '23.'] ['Innere Stadt', 'Leopoldstadt', 'Landstraße', 'Wieden', 'Margareten', 'Mariahilf', 'Neubau', 'Josefstadt', 'Alsergrund', 'Favoriten', 'Simmering', 'Meidling', 'Hietzing', 'Penzing', 'Rudolfsheim-Fünfhaus', 'Ottakring', 'Hernals', 'Währing', 'Döbling', 'Brigittenau', 'Floridsdorf', 'Donaustadt', 'Liesing'] ['1010 Wien', '1020 Wien', '1030 Wien', '1040 Wien', '1050 Wien', '1060 Wien', '1070 Wien', '1080 Wien', '1090 Wien', '1100 Wien', '1110 Wien', '1120 Wien', '1130 Wien', '1140 Wien', '1150 Wien', '1160 Wien', '1170 Wien', '1180 Wien', '1190 Wien', '1200 Wien', '1210 Wien', '1220 Wien', '1230 Wien']
In [17]:
# Create a pandas dataframe
df_bezirkes = pd.DataFrame({'BezirkeNbr':ordernumbers, 'Bezirke':names, 'Zip':addresses})
df_bezirkes.head(23)
Out[17]:
BezirkeNbr Bezirke Zip
0 1. Innere Stadt 1010 Wien
1 2. Leopoldstadt 1020 Wien
2 3. Landstraße 1030 Wien
3 4. Wieden 1040 Wien
4 5. Margareten 1050 Wien
5 6. Mariahilf 1060 Wien
6 7. Neubau 1070 Wien
7 8. Josefstadt 1080 Wien
8 9. Alsergrund 1090 Wien
9 10. Favoriten 1100 Wien
10 11. Simmering 1110 Wien
11 12. Meidling 1120 Wien
12 13. Hietzing 1130 Wien
13 14. Penzing 1140 Wien
14 15. Rudolfsheim-Fünfhaus 1150 Wien
15 16. Ottakring 1160 Wien
16 17. Hernals 1170 Wien
17 18. Währing 1180 Wien
18 19. Döbling 1190 Wien
19 20. Brigittenau 1200 Wien
20 21. Floridsdorf 1210 Wien
21 22. Donaustadt 1220 Wien
22 23. Liesing 1230 Wien
In [18]:
df_bezirkes.to_csv('bezirkes.csv', index=False, encoding='utf8') # save it (just for a case)

Lopuksi yhdistetään raavitusta datasta muodostettu kaupunginosanumerot sisältävä dataframe kohteet sisältävään dataframeen.

In [19]:
# join this dataframe to the listing datafarame
df = df.join(df_bezirkes.set_index('Bezirke'), on = 'neighbourhood_cleansed') 
In [20]:
df.head(2)
Out[20]:
id listing_url neighbourhood_cleansed latitude longitude room_type accommodates bedrooms beds price availability_30 number_of_reviews review_scores_rating instant_bookable cancellation_policy reviews_per_month BezirkeNbr Zip
0 15883 https://www.airbnb.com/rooms/15883 Donaustadt 48.24144 16.42812 Hotel room 3 1.0 1.0 $85.00 21 10 96.0 t moderate 0.18 22. 1220 Wien
1 38768 https://www.airbnb.com/rooms/38768 Leopoldstadt 48.21823 16.37926 Entire home/apt 5 1.0 3.0 $65.00 10 303 95.0 f strict_14_with_grace_period 2.87 2. 1020 Wien

Talletetaan dataframe tiedostoon, jotta sen sisältöä voi halutessaan tarkastella myös muilla työkaluilla (esimerkiksi taulukkolaskentaohjelmalla).

In [21]:
df.to_csv('data.csv', index='False')

len(df.index)
Out[21]:
12615

Kenttä ’ neighbourhood_cleansed’ sisältää kohteen kaupunginosan osan nimen. Kun tarkastellaan sen jakaumaa, havaitaan jakauman olevan Wienin datassa yllättävän tasainen: vain yhdessä kaupunginosassa on alle 100 kohdetta ja enimmillään kohteita on 1300. Kaupunginosia on yhteensä 23, joten kaupunginosajakokaan ei ole liian tarkka. Tämä kenttä vaikuttaa tässä aineistossa varsin käyttökelpoiselta, sillä eri kaupunginosista on varsin mukavasti rivejä. Esimerkissä käyytetty San Franciscon data oli paljon epätasaisemmin jakautunut.

In [22]:
nb_counts = Counter(df.neighbourhood_cleansed)
tdf = pd.DataFrame.from_dict(nb_counts, orient='index').sort_values(by=0)
In [23]:
# Redefining visualization width
plt.rcParams["figure.figsize"] = [20, 5]
tdf.plot(kind='bar')
Out[23]:
<matplotlib.axes._subplots.AxesSubplot at 0x18802870e88>
In [24]:
len(nb_counts)
Out[24]:
23
In [25]:
df_nb = pd.DataFrame.from_dict(nb_counts, orient='index', columns=['count'])
df_nb.columns
Out[25]:
Index(['count'], dtype='object')
In [26]:
df_nb.sort_values(by=['count'], ascending=False).head(12)
Out[26]:
count
Leopoldstadt 1332
Landstraße 1135
Alsergrund 853
Rudolfsheim-Fünfhaus 832
Margareten 784
Neubau 777
Innere Stadt 719
Favoriten 673
Mariahilf 631
Ottakring 604
Wieden 575
Brigittenau 482

3. Datan jalostaminen

Dataa tuli jalostettua jo edellisessä tarkasteluvaiheessa, kun korjasin kentän 'neighbourhood_cleansed' merkkijonokoodauksen. Kyseinen toimenpide oli kuitenkin lähinnä kosmeettinen; sen ainoa toiminnallinen merkitys liittyi siihen, että sitä käytettiin avainkenttänä yhdistettäessä kaksi datalähdettä. Tällaisia kosmeettisilta vaikuttavia jalostustoimenpiteitä ei kannata kuitenkaan aliarvioida, sillä datan esittäminen oikeassa muodossa on tärkeää ymmärrettävyydenkin vuoksi, kun dataa tutkitaan esimerkiksi visualisointien avulla. Tätä dataa täytyy toki hieman siivota ja jalostaa myös varsinaisista toiminnallisista syistä ennen kuin sitä kannattaa tarkemmin visualisoida ja ennen kuin sen avulla voi opettaa mallia. Puuttuvat arvot pitää imputoida tai puuttuvia arvoja sisältävät rivit pitää poistaa ja kategoriset (ei-numeeriset) arvot pitää korvata numeerisilla arvoilla.

Sarakkeessa 'reviews_per_month' on paljon puuttuvia arvoja. Tarkasteltaessa sarakkeiden 'number_of_reviews' ja 'reviews_per_month' arvoja havaitaan, että sarakkeessa 'reviews_per_month' on NaN-arvo ainoastaan silloin, kun sarakkeessa 'number_of_reviews' on nolla. Näin ollen sarakkeen 'reviews_per_month' NaN-arvot voidaan muuttaa nolliksi. Näin myös myöhemmin tehdään. Ensimmäinen alla olevista lausekkeista tarkistaa, ettei 'reviews_per_month' ole koskaan muuta kuin NaN silloin kun 'number_of_reviews' on nolla. Toinen lausekkeista tarkistaa, ettei 'reviews_per_month' ole koskaan NaN silloin kun 'number_of_reviews' poikkeaa nollasta. NaN-arvot ja arvioiden puuttuminen ovat siis ekvivalentit.

In [27]:
# the number of entries with 0 'number_of_reviews' which do not a NaN for 'reviews_per_month'
len(df[((df.number_of_reviews == 0) & (pd.isnull(df.number_of_reviews) == False)
       & (pd.isnull(df.reviews_per_month) == False))].index)
Out[27]:
0
In [28]:
# the number of entries with at least 1 'number_of_reviews' which have a NaN for 'reviews_per_month'
len(df[(df.number_of_reviews != 0) & (pd.isnull(df.number_of_reviews) == False)
       & (pd.isnull(df.reviews_per_month) == True)].index)
Out[28]:
0

Korvataan kaikki NaN-arvot arvolla 0 sarakkeessa 'reviews_per_month', koska näistä kohteista ei ole arvioita. Lisäksi poistetaan aineistosta sellaiset epäilyttävät kohteet, joissa ei ole makuuhuoneita (0 sarakkeessa 'bedrooms') tai vuoteita (0 sarakkeessa 'beds'). Myöhemmin poistetaan myös kohteet, joiden hinta on 0, mutta hintasarakkeesta pitää poistaa $-merkki ja se pitää muuttaa numeeriseksi ennen kuin 0-hintaiset kohteet kannattaa poistaa. Alkuperäisessä esimerkissä tämä hintasuodatus tehty väärin jo tässä kohdassa ilman $-merkkiä, eikä se siten poista 0-hintaisia kohteita. Lopuksi poistetaan vielä aineistosta kaikki kohteet, joihin on jäänyt NaN-arvo johonkin sarakkeeseen. Kaikkia kohteita ei olisi välttämätöntä poistaa, vaan voitaisiin myös yrittää imputoida puuttuvia arvoja muiden kohteiden arvojen avulla. Esimerkiksi sarakkeen 'review_scores_rating' NaN-avot voitaisiin imputoida vaikkapa sen muista kohteista laketulla keskiarvolla. Näin saataisiin muiden sarakkeiden suurempi aineisto ennustusmallinopetukseen, mutta visalisointiin tulisi näkyviin tekaistuja arviointintilukemia, mikä ei ole suotavaa.

In [29]:
# so we need to do some cleaning.

# first fixup 'reviews_per_month' where there are no reviews
df['reviews_per_month'].fillna(0, inplace=True)

# just drop rows with bad/weird values
# (we could do more here)
df = df[df.bedrooms != 0]
df = df[df.beds != 0]
#df = df[df.price != 0]    # TÄTÄ EI VOI TEHDÄ VIELÄ TÄSSÄ, sillä ensin on poistettava $-merkit

# 'review_score_rating' NaN-arvot voisi myös imputoida esimerkiksi näin:
imputer = impute.SimpleImputer(strategy='median')  
#df.review_scores_rating = imputer.fit_transform(df.review_scores_rating.values.reshape(-1, 1)).reshape(-1, 1)

df = df.dropna(axis=0)  # poistetaan kuitenkin kaikki rivivit, joissa on jäljellä NaN-arvoja
                        # tarkastellaan kaikkia asuntoja toisin kuin alkuperäisessä esimerkissä 
len(df.index)
Out[29]:
9347
In [30]:
df.price.head(5)
Out[30]:
0     $85.00
1     $65.00
2    $130.00
4     $59.00
5     $50.00
Name: price, dtype: object
In [31]:
# remove the $ from the price and convert to float
df['price'] = df['price'].replace('[\$,)]','',  \
        regex=True).replace('[(]','-', regex=True).astype(float)
df.price.sort_values(ascending=False).head(61)
Out[31]:
1258     9270.0
9735     5000.0
3275     1900.0
9241     1000.0
8924     1000.0
          ...  
11796     515.0
11776     515.0
11777     515.0
8599      502.0
8386      500.0
Name: price, Length: 61, dtype: float64
In [32]:
df.price.sort_values(ascending=True).head(100)
Out[32]:
2025      9.0
7892      9.0
6631      9.0
5492     10.0
7539     10.0
         ... 
1387     17.0
7233     17.0
3253     17.0
6003     17.0
10676    17.0
Name: price, Length: 100, dtype: float64

Nyt $-merkit on poistettu hintakentästä ja se on muutettu liukuluvuksi, joten dataframesta voidaan poistaa ne rivit, joista hinta puuttuu. Näitä tosin ei näytä olevan tässä aineistossa. Aineistossa näyttää olevan 60 kohdetta, joiden hinta on yli 500. Poistetaan nämä kalliit asunnot dataframesta, koska nämä eivät kiinnostane normaalia asunnon vuokraajaa ja jotta jakaumista saadaan tehtyä informatiivisempia kuvia. Tarkastellaan kuitenkin ensin, missä kalliit asunnot sijaitsevat. Alla olevasta listauksesta nähdään, että kalliita asuntoja on yllättäen ympäri kaupunkia, joskin ne painottuvat eskusta-alueelle. Niiden merkitys on kuitenkin varsin vähäinen analysoitaessa kaupunginosien hintatasoja.

In [33]:
df[df.price > 500].groupby(
     ['neighbourhood_cleansed']
  ).agg(
    count=('id', len)
).sort_values(by=['count'], ascending=False)
Out[33]:
count
neighbourhood_cleansed
Innere Stadt 16
Leopoldstadt 10
Landstraße 6
Alsergrund 5
Margareten 5
Josefstadt 4
Ottakring 4
Wieden 3
Döbling 2
Währing 2
Donaustadt 1
Hietzing 1
Rudolfsheim-Fünfhaus 1
In [34]:
# Nyt voi tehdä hintaan perustuvat suodatukset
df = df[df.price != 0] # turha Wien-aineistossa 19.11.2019
df = df[df.price <= 500]
len(df)
Out[34]:
9287
In [35]:
# Talletetaan siivottu data PowerBI:ta varten visualisoitavaksi
df.to_csv('data_windows.csv', encoding='windows-1252')

Osa mukaan valituista piirteistä on kategorisia, joten niiden avulla ei voi suoraan opettaa ennustemallia. Joukossa on myös pelkästään visualisointia varten mukaan otettuja muuttujia, jota voidaan tiputtaa pois dataframesta ennen opettamista. Tällaisia ovat esimerkiksi kohteen url sekä kaupunginosanumero ja postiosoite.

Selittäjiksi suunnitellut kategoriset piirteet pitää kuitenkin muuttaa numeerisiksi. Merkkijonomuotoisen kentän 'neighborhood_cleansed' tietosisältö saadaan muutettua numeeriseksi ns. dummy-muuttujien avulla käyttämällä Pandaksen funktiota get_dummies. Menetelmää kutsutaan nimellä "one hot encoding". Siinä jokaista sarakkeen merkkijonoarvoa kohden muodostetaan uusi sarake. Kunkin rivin merkkijonoa vastaavaan uutteen sarakkeeseen sijoitetaan arvo 1 ja kaikkiin muihin uusiin muodostettuihin sarakkeisiin sijoitetaan arvo 0. Samaa menetelmää käytetään myös merkkijonomuotoisille kentille 'room_type' ja 'cancellation_policy'.

Kenttä 'instant_bookable' on myös muodollisesti merkkijono, mutta sillä on vain kaksi arvoa 't' ja 'f', joten tosiasiallisesti se kuvaa totuusarvoa. Siitä generoidaan myös ensin uudet sarakkeet samalla menetelmällä, mutta muodostuviin lisätään prefix 'instant'. Koska muodostuvat kaksi saraketta ovat toistensa vastakohdat, poistetaan lopuksi arvoja epätosi edustava sarake. Kaikki funktiolla get_dummies muodostetut sarakkeet pitää lopuksi lisätä piirteet sisältävään dataframeen (alldata), ja niitä vastaavat kategoristen muuttujien sarakkeet pitää vastaavasti tiputtaa pois dataframesta.

In [36]:
# get feature encoding for categorical variables
n_dummies = pd.get_dummies(df.neighbourhood_cleansed)
rt_dummies = pd.get_dummies(df.room_type)
xcl_dummies = pd.get_dummies(df.cancellation_policy)

# convert boolean column to a single boolean value indicating whether this listing has instant booking available
ib_dummies = pd.get_dummies(df.instant_bookable, prefix="instant")
ib_dummies = ib_dummies.drop('instant_f', axis=1)

# replace the old columns with our new one-hot encoded ones
alldata = pd.concat((df.drop(['neighbourhood_cleansed', \
    'room_type', 'cancellation_policy', 'instant_bookable'], axis=1), \
    n_dummies.astype(int), rt_dummies.astype(int), \
    xcl_dummies.astype(int), ib_dummies.astype(int)), \
    axis=1)
allcols = alldata.columns
alldata.head(5)
Out[36]:
id listing_url latitude longitude accommodates bedrooms beds price availability_30 number_of_reviews ... Hotel room Private room Shared room flexible moderate strict strict_14_with_grace_period super_strict_30 super_strict_60 instant_t
0 15883 https://www.airbnb.com/rooms/15883 48.24144 16.42812 3 1.0 1.0 85.0 21 10 ... 1 0 0 0 1 0 0 0 0 1
1 38768 https://www.airbnb.com/rooms/38768 48.21823 16.37926 5 1.0 3.0 65.0 10 303 ... 0 0 0 0 0 0 1 0 0 0
2 40625 https://www.airbnb.com/rooms/40625 48.18486 16.32740 6 2.0 4.0 130.0 19 149 ... 0 0 0 0 1 0 0 0 0 1
4 70568 https://www.airbnb.com/rooms/70568 48.22224 16.42460 2 1.0 1.0 59.0 30 10 ... 0 0 0 0 0 0 1 0 0 0
5 70637 https://www.airbnb.com/rooms/70637 48.21781 16.38023 2 1.0 2.0 50.0 5 111 ... 0 1 0 0 1 0 0 0 0 0

5 rows × 48 columns

4. Datan kuvaileminen

Ensimmäiseksi kuvailin asuntojen hintoja alueittain boxplot-kaavion avulla. Kaavion luettavuuden vuoksi siihen ei voi ottaa mukaan kaikkia 23 kaupunginosaa. Ei myöskään ole mielekästä tarkastella samalla asteikolla 50 ja yli 500 kohteen kaupunginosia. Kaaviota varten muodostetaan ensin indeksitaulukko 'top_neighbourhoods', johon otetaan mukaan ne kaupunginosat, joissa on alkuperäisessä aineistossa yli 500 vuokrauskohdetta. Indeksitaulukko lajitellaan, ja sen avulla poimitaan siivotusta (kategoriset muuttujat sisältävästä) dataframesta näiden suositumpien kaupunginosien kohteet dataframeen 'df_top'. Tästä dataframesta piirretään boxplot-kaavio kaupunginosien hintatasosta kirjaston Seaborn funktiolla boxplot. Kaaviosta nähdään, että vuokrauskohteiden hinnat ovat jakautuneet varsin samalla tavalla kaikissa muissa paitsi 1. kaupunginosassa Innere Stadt, eli vain aivan ydinkeskustan kohteet ovat selvästi muita kalliimpia.

In [37]:
# Some kind of a boxplot 
df.price.value_counts()
top_neighbourhoods= df_nb[df_nb['count'] > 500].sort_values(by=['count'],ascending=False).index
df_top = df[df.neighbourhood_cleansed.isin(top_neighbourhoods)]
df_top.head()
Out[37]:
id listing_url neighbourhood_cleansed latitude longitude room_type accommodates bedrooms beds price availability_30 number_of_reviews review_scores_rating instant_bookable cancellation_policy reviews_per_month BezirkeNbr Zip
1 38768 https://www.airbnb.com/rooms/38768 Leopoldstadt 48.21823 16.37926 Entire home/apt 5 1.0 3.0 65.0 10 303 95.0 f strict_14_with_grace_period 2.87 2. 1020 Wien
2 40625 https://www.airbnb.com/rooms/40625 Rudolfsheim-Fünfhaus 48.18486 16.32740 Entire home/apt 6 2.0 4.0 130.0 19 149 97.0 t moderate 1.32 15. 1150 Wien
5 70637 https://www.airbnb.com/rooms/70637 Leopoldstadt 48.21781 16.38023 Private room 2 1.0 2.0 50.0 5 111 95.0 f moderate 1.05 2. 1020 Wien
6 75471 https://www.airbnb.com/rooms/75471 Ottakring 48.22227 16.31540 Entire home/apt 4 2.0 2.0 74.0 0 50 97.0 t moderate 0.50 16. 1160 Wien
8 78416 https://www.airbnb.com/rooms/78416 Rudolfsheim-Fünfhaus 48.19952 16.33075 Entire home/apt 4 1.0 2.0 55.0 12 177 87.0 t strict_14_with_grace_period 1.68 15. 1150 Wien
In [38]:
import seaborn as sns
AX = sns.boxplot(x='neighbourhood_cleansed', y='price', data=df_top)

Seuraavaksi oli karttavisualisointien vuoro, sillä asuntokohteiden tarkastelussa asunnon sijainti ja sen ympäristö kiinnostavat yleensä erittäin paljon vuokraajaa. Päätin käyttää näihin visualisointeihin Teppo Kivennon löytämää kirjastoa Folium. Kirjastoa ei ollut valmiiksi Anacondassa, joten asensin sen oheistuksen mukaisesti (conda install folium -c conda-forge). Netistä löytyi aika paljon esimerkkejä tämän kirjaston käyttämisestä (esimerkiksi Quickstart, kaggle ja Medium). Toteutin Folium-kirjaston avulla kohteiden lämpökartan, kohdekartan ja kaupunginosakartan. Kohdekartassa käytetään kohdeklusterointia ryhmittelemään yksittäisiä kohteita eri zoomaustasoilla. Klusterointi muuttuu automattisesti zoomatessa.

Lämpökartta antaa aika karkean kuvan kohteista, mutta zoomaamalla siitäkin saa jotain informaatiota. Lämpökartta talletetaan tiedostoon heat_map.html. Klusteroidun kohdekartan avulla on helpompi hahmottaa kohteiden sijaiti ja niitä pääsee myös tarkastelemaan. Liitin kohteisiin popup-tulosteet, jotka sisältävät kohteen hinnan, majoituskapasiteetin ja Airbnb-linkin. Kun linkkiä klikkaa popupista, avautuu kohteen Airbnb-sivu uuteen selaimen välilehteen. Chrome ei tulostanut kuvaa Jupyterissa, jos siinä on yli 2000 popup-tulostetta, mutta talletetusta html-tiedostosta (listing_map.html) se generoi kuvan erilliseen ikkunaan. Firefoxissa kuva tulostui myös Jupyter Notebookissa, vaikka kaikkiin kohteisiin liittyi popup. Tämän seurauksena siirryin käyttämään Firefoxia Jupyter Notebookin kanssa.

Kohdekartan lisäksi tein foliumilla myös karttavisualisoinnin kaupunginosista Inside Airbnb -datasetin sisältämän geojson-datan avulla. Valitettavasti tässäkin tiedostossa oli taas kaupunginosien nimissä sama kummallinen merkkikoodaus, ja jouduin taas konvertoimaan merkkijonot. Määritin kaupunginosille eri värit, jotta ne erottuvat selkeästi kartassa. Lisäsin karttaan jokaisen kaupunginosan kohdalle kohteiden yhteenvetotietoja sisältävän popup-tulosteen. Popup-tuloste sisältää kohteiden keskihinnan, arvioiden keskiarvon sekä kohteiden lukumäärän yhteensä ja kohdetyypeittäin. Tämän kartan avulla saa yleiskatsauksen kohteiden hintatasosta ja lukumääristä eri puolilla Wieniä ja voi tarkastella kaupunginosien välisiä eroja. Tallensin tämän kartan tiedostoon bezirke_map.html.

Tarkastelin myös Wienin kaupungin avointa dataa ja totesin, että tarjolla oli runsaasti erilaisia paikkatietoja sisältäviä datalähteitä. Koska Wienissä on valtavasti erilaisia museoita, päätin kokeilla lisätä kaupunginosakarttaan löytämäni museolistauksen kohteet. Generoin tämän kartan uusteen ikkunaan, sillä merkintöjä oli niin paljon, ettei keskikaupungin kaupunginosiin pystynyt enää kohdistamaan osumatta museomerkintään, jollei zoomannut lähemmäksi. Museoista kiinnostunut asunnon vuokraaja voi valita asunnoin kaupunginosan tämän kartan avulla. Tämä kartta löytyy talletettuna tiedostosta museum_map.html. Kartat tulostuvat Jupyter Notebooksissa vain sillä istuntokerralla, jolla ne generoidaan. Ne eivät näy myöskään GitHubissa avattaessa Jupyter Notebook -tiedosto. Edellä mainituista tiedostoista kartat ovat suoraan ladattavissa selaimeen, ja karttatiedostot ovat myös ladattavissa GitHubista. Suosittelen käyttämään Firefoxia.

In [39]:
map = folium.Map(location=[48.210033, 16.363449], zoom_start = 12)
#folium.TileLayer("OpenStreetMap").add_to(map)

coordinates = np.array([alldata.latitude.to_list(), alldata.longitude.to_list()]).T 
                                                                 # transpose matrix
HeatMap(coordinates).add_to(map)
map.save('heat_map.html')
map
Out[39]:
In [40]:
map = folium.Map(location=[48.210033, 16.363449], zoom_start = 12)

marker_cluster_group = MarkerCluster(name="Airbnb").add_to(map)

for row in alldata.iterrows():
    text = ('Price: ' + str(row[1].price) + '<br>' +
            'Accommodates: ' + str(row[1].accommodates) + '<br>' + 
            '<a href =\"' + str(row[1].listing_url) + '\" target=\"_blank\">' + 
                            str(row[1].listing_url) + '</a></p>'
           )
    folium.Marker(location=[row[1].latitude, row[1].longitude],
                  popup = folium.Popup(text, max_width=250)).add_to(marker_cluster_group)

marker_cluster_group.add_to(map)
map.save('listing_map.html')
map
Out[40]:
In [41]:
map = folium.Map(location=[48.210033, 16.363449], zoom_start = 11)
folium.TileLayer('stamenterrain').add_to(map) # kokeillaan maastomuotokarttaa

fp = urllib.request.urlopen(geojson_file) # noudetaan geojson-tiedosto suoraan netistä
geodata = json.load(fp)

bezirkeFillColors = ['#0000ff','#00ff00','#ff0000','#00ffff','#ffff00','#ff00ff',
                     '#7700ff','#77ff00','#770000','#77ffff','#77ff00',
                     '#0077ff','#007700','#ff7700','#0077ff','#ff7700',
                     '#000077','#00ff77','#ff7700','#00ff77','#ffff77',
                     '#ff7777','#000000']
i = 0 # väri-indeksi

for feature in geodata['features']:
    geo_n = folium.GeoJson(
        feature,
        style_function = lambda x, fillColor = bezirkeFillColors[i]: {
            'fillColor': fillColor,
            'color': 'black'
        }
    )
    bezirke = feature['properties']['neighbourhood'] 
    # Kummallisen koodauksen vuoksi pitää taas muuttaa merkkejä:
    bezirke = bezirke.translate(bezirke.maketrans('\u008A\u009A\u009F\u0080\u0085\u0086\u00A7',
                                                  'äöüÄÖÜß'))
    bezdata = alldata[alldata[bezirke] == 1] # haetaan rivit, joiden kaupunginosasarakkeessa 1
    text = (
        '<h5><b>' + bezirke + '</b></h5>' +
        'Average Price: ' + str(round(bezdata.price.mean(),2)) + '<br>' +
        'Average Rating: ' + str(round(bezdata.review_scores_rating.mean())) + ' (of 100)<br>'+
        'Apartments: ' + str(len(bezdata)) + '<br>' +
        '<ul><li>entire apartment: ' + 
                               str(len(bezdata[bezdata['Entire home/apt'] == 1])) + '</li>' +
        '<li>private room: ' + str(len(bezdata[bezdata['Private room'] == 1])) + '</li>' +
        '<li>shared room: ' + str(len(bezdata[bezdata['Shared room'] == 1])) + '</li>' +
        '<li>hotel room: ' + str(len(bezdata[bezdata['Hotel room'] == 1])) + '</li></ul>'
    )
    geo_n.add_child(folium.Popup(text, max_width=250))
    geo_n.add_to(map)   
    i = i + 1

map.save('bezirke_map.html')
map
Out[41]:
In [42]:
fp = urllib.request.urlopen('https://data.wien.gv.at/daten/geo?service=WFS&request=' + 
                            'GetFeature&version=1.1.0&typeName=ogdwien:MUSEUMOGD&srsName=' +
                            'EPSG:4326&outputFormat=json')
museumdata = json.load(fp)
folium.TileLayer('OpenStreetMap').add_to(map)

for museum in museumdata['features']:
    folium.Marker(location=[museum['geometry']['coordinates'][1], 
                            museum['geometry']['coordinates'][0]],
                  popup=folium.Popup(museum['properties']['NAME'], max_width=150),
                  icon = folium.Icon(color='red', icon='home')).add_to(map)
    

map.save('museum_map.html')
map
Out[42]:

Seuraavaksi tarkastelin eri piirteiden välisiä korrelaatioita. Pandaksen avulla saa piirrettyä lämpöväritetyn korrelaatiomatriisin, joka on varsin informatiivinen ja josta helposti löytää vahvimmat positiiviset ja negatiiviset korrelaatiot. Matriisi on varsin laaja, mutta erityisesti silmään pistää varsin vahva negatiivinen korrelaatio kohdetyyppien "Private room" ja "Entire home/apt" välillä, mikä käytännössä tarkoittaa kyseisten muuttujien olevan toistensa vastakohdat. Koska nämä muuttujat käytännössä suoraan määräävät toistensa arvot, niin vain toista niistä voi käyttää selittäjänä; lienee aika lailla yhdentekevää, kumpi otetaan selittäjäksi. Majoituskapasiteetilla sekä makuuhuoneiden ja sänkyjen lukumäärällä näyttää myös olevan sen verran vahva positiivinen korrelaatio, että varsinkin niistä kannattaa piirtää hajontapistekaaviot tarkempaa tarkastelua varten.

In [43]:
# Lämpöväritetty korrelaatiomatriisi
corr = alldata.corr(method='pearson')
corr.style.background_gradient(cmap='coolwarm').set_precision(3)
Out[43]:
id latitude longitude accommodates bedrooms beds price availability_30 number_of_reviews review_scores_rating reviews_per_month Alsergrund Brigittenau Donaustadt Döbling Favoriten Floridsdorf Hernals Hietzing Innere Stadt Josefstadt Landstraße Leopoldstadt Liesing Margareten Mariahilf Meidling Neubau Ottakring Penzing Rudolfsheim-Fünfhaus Simmering Wieden Währing Entire home/apt Hotel room Private room Shared room flexible moderate strict strict_14_with_grace_period super_strict_30 super_strict_60 instant_t
id 1 -0.0331 -0.0163 0.00977 0.0129 -0.0316 0.027 0.137 -0.362 0.00138 0.282 -0.0251 -0.0113 0.0161 0.00196 0.0772 0.0158 0.0421 0.00891 -0.0242 -0.0371 -0.00956 -0.00725 0.0303 -0.0343 -0.0257 0.0519 -0.0514 0.0357 0.0313 0.00962 -0.00106 -0.0218 0.00136 -0.0445 0.0188 0.0389 0.0104 0.153 -0.0798 0.0167 -0.067 0.0165 0.0105 0.239
latitude -0.0331 1 0.241 0.00768 -0.00497 0.0235 -0.0132 -0.00863 -0.0239 0.0328 -0.0552 0.224 0.268 0.241 0.318 -0.374 0.325 0.122 -0.13 0.041 0.0483 -0.0983 0.237 -0.242 -0.231 -0.132 -0.264 -0.037 0.0631 -0.0748 -0.157 -0.176 -0.147 0.192 -0.0186 0.00288 0.016 0.0115 0.00575 -0.0194 -0.00574 0.0131 0.00645 0.0152 0.00538
longitude -0.0163 0.241 1 0.0596 0.0442 0.0449 0.0352 0.0266 0.0445 -0.00959 0.0493 -0.027 0.0847 0.487 -0.062 0.1 0.168 -0.166 -0.254 0.0811 -0.0716 0.308 0.293 -0.139 -0.0362 -0.0746 -0.17 -0.114 -0.233 -0.345 -0.241 0.185 0.0593 -0.121 0.034 -0.0107 -0.0349 0.014 -0.0182 0.0069 -0.0196 0.0102 0.00842 -0.0051 0.0226
accommodates 0.00977 0.00768 0.0596 1 0.71 0.781 0.515 0.246 0.0632 -0.0705 0.073 -0.011 0.0604 0.0251 0.0101 0.0415 -0.00501 -0.0104 -0.00706 0.104 -0.0375 0.0262 -0.0306 0.00194 -0.00598 -0.0216 -0.0241 -0.047 -0.0178 -0.0159 0.000718 -0.0133 0.00765 -0.0442 0.415 0.0232 -0.426 -0.0181 -0.127 -0.00484 -0.00832 0.123 0.0299 -0.00421 0.158
bedrooms 0.0129 -0.00497 0.0442 0.71 1 0.66 0.458 0.161 0.00927 -0.0369 0.0104 -0.0166 0.00822 0.0417 0.0137 0.0208 0.00716 -0.0143 0.00642 0.0805 -0.0212 0.00622 -0.0312 0.0342 -0.00824 -0.00223 -0.0218 -0.0373 -0.000396 -0.011 -0.02 -0.0128 0.0384 -0.0221 0.269 0.0051 -0.267 -0.0442 -0.07 -0.00328 -0.00534 0.069 0.0107 -0.00756 0.0568
beds -0.0316 0.0235 0.0449 0.781 0.66 1 0.406 0.209 0.0376 -0.078 0.0228 -0.0247 0.067 0.038 0.0204 0.0151 0.00517 0.00604 -0.00218 0.0592 -0.0398 -0.0138 -0.00811 0.00695 0.0015 -0.0148 -0.0153 -0.0387 -0.0104 0.000331 0.00217 0.0129 0.00235 -0.0342 0.277 0.0222 -0.301 0.0613 -0.086 -0.0155 -0.000275 0.0946 0.0263 -0.000389 0.106
price 0.027 -0.0132 0.0352 0.515 0.458 0.406 1 0.23 -0.0316 -0.0465 -0.0464 0.00847 -0.000516 -0.0266 -0.0126 0.0193 -0.0358 -0.0505 0.0136 0.272 -0.0183 0.0048 -0.0339 -0.00612 -0.0534 0.00505 -0.0141 -0.00681 -0.0331 -0.046 -0.0322 -0.00957 0.0187 -0.0207 0.336 0.0775 -0.353 -0.0484 -0.155 -0.0391 -0.00623 0.183 0.036 0.00105 0.119
availability_30 0.137 -0.00863 0.0266 0.246 0.161 0.209 0.23 1 0.0272 -0.0907 0.135 -0.0296 -0.00498 0.072 0.0422 0.0607 0.0507 -0.00838 0.0259 0.0194 -0.041 -0.0247 -0.0163 0.0445 -0.035 -0.0255 0.0215 -0.076 -0.000715 0.0545 0.0273 0.0254 -0.024 -0.0148 0.118 0.0803 -0.149 0.0502 -0.0845 -0.00736 0.0145 0.0846 0.0288 -0.00085 0.14
number_of_reviews -0.362 -0.0239 0.0445 0.0632 0.00927 0.0376 -0.0316 0.0272 1 0.029 0.563 -0.00842 -0.0221 -0.0272 -0.0439 -0.0218 -0.0263 -0.0349 -0.027 0.0823 -0.00349 0.0563 0.0145 -0.0194 -0.0132 -0.000285 -0.0259 0.0485 -0.0167 -0.039 0.00155 -0.0286 0.0545 -0.04 0.0882 -0.0139 -0.0841 -0.0136 -0.162 0.04 -0.00603 0.116 -0.0249 -0.009 0.151
review_scores_rating 0.00138 0.0328 -0.00959 -0.0705 -0.0369 -0.078 -0.0465 -0.0907 0.029 1 0.0492 0.0324 0.0115 0.0144 0.00374 -0.0337 0.00537 -0.0285 -0.00832 -0.00213 0.00605 0.02 -0.031 -0.00169 -0.0184 0.0169 -0.00981 0.0194 0.000713 0.00362 -0.00839 -0.0373 0.0165 0.0123 0.0255 -0.0375 -0.0135 -0.0186 -0.0375 0.0713 0.0023 -0.0333 -0.0288 -0.00411 -0.0964
reviews_per_month 0.282 -0.0552 0.0493 0.073 0.0104 0.0228 -0.0464 0.135 0.563 0.0492 1 -0.0233 -0.0289 -0.0136 -0.0501 0.0541 -0.0132 -0.00557 -0.0304 0.0418 -0.0229 0.0548 0.0201 0.0034 -0.0264 -0.01 0.0245 -0.0132 0.0159 -0.0179 -0.00748 -0.0128 0.0251 -0.0486 0.069 -0.0147 -0.064 -0.0154 -0.0769 0.00482 0.0152 0.0708 -0.0336 -0.0118 0.311
Alsergrund -0.0251 0.224 -0.027 -0.011 -0.0166 -0.0247 0.00847 -0.0296 -0.00842 0.0324 -0.0233 1 -0.0508 -0.0439 -0.0402 -0.0599 -0.0283 -0.0454 -0.0278 -0.0634 -0.0489 -0.0829 -0.088 -0.0191 -0.0657 -0.0611 -0.0503 -0.0693 -0.0576 -0.044 -0.0668 -0.0278 -0.0575 -0.0463 -0.0315 0.0615 0.0187 -0.00644 0.0111 -0.0256 -0.00267 0.0151 0.000737 -0.00378 0.0093
Brigittenau -0.0113 0.268 0.0847 0.0604 0.00822 0.067 -0.000516 -0.00498 -0.0221 0.0115 -0.0289 -0.0508 1 -0.0336 -0.0308 -0.0458 -0.0217 -0.0347 -0.0213 -0.0485 -0.0374 -0.0634 -0.0673 -0.0146 -0.0502 -0.0467 -0.0385 -0.053 -0.044 -0.0336 -0.0511 -0.0213 -0.044 -0.0354 0.0517 -0.021 -0.0458 -0.0103 -0.0226 0.011 -0.00204 0.00649 0.0486 -0.00289 0.0154
Donaustadt 0.0161 0.241 0.487 0.0251 0.0417 0.038 -0.0266 0.072 -0.0272 0.0144 -0.0136 -0.0439 -0.0336 1 -0.0266 -0.0396 -0.0187 -0.03 -0.0184 -0.0419 -0.0323 -0.0548 -0.0582 -0.0127 -0.0434 -0.0404 -0.0333 -0.0458 -0.0381 -0.0291 -0.0442 -0.0184 -0.038 -0.0306 -0.00856 0.00659 0.00705 0.000623 0.0107 0.00452 -0.00177 -0.0139 -0.00685 -0.0025 0.0143
Döbling 0.00196 0.318 -0.062 0.0101 0.0137 0.0204 -0.0126 0.0422 -0.0439 0.00374 -0.0501 -0.0402 -0.0308 -0.0266 1 -0.0363 -0.0172 -0.0275 -0.0169 -0.0384 -0.0296 -0.0502 -0.0534 -0.0116 -0.0398 -0.037 -0.0305 -0.042 -0.0349 -0.0267 -0.0405 -0.0169 -0.0349 -0.0281 -0.0336 -0.00319 0.0281 0.0363 0.00666 0.0234 -0.00162 -0.029 -0.00628 -0.00229 -0.0166
Favoriten 0.0772 -0.374 0.1 0.0415 0.0208 0.0151 0.0193 0.0607 -0.0218 -0.0337 0.0541 -0.0599 -0.0458 -0.0396 -0.0363 1 -0.0256 -0.041 -0.0251 -0.0572 -0.0442 -0.0749 -0.0795 -0.0173 -0.0593 -0.0552 -0.0454 -0.0626 -0.052 -0.0397 -0.0603 -0.0251 -0.052 -0.0418 0.0164 0.00305 -0.0181 0.0029 -0.0157 0.0156 -0.00241 -0.000818 0.00279 -0.00342 0.0387
Floridsdorf 0.0158 0.325 0.168 -0.00501 0.00716 0.00517 -0.0358 0.0507 -0.0263 0.00537 -0.0132 -0.0283 -0.0217 -0.0187 -0.0172 -0.0256 1 -0.0194 -0.0119 -0.0271 -0.0209 -0.0354 -0.0376 -0.00817 -0.028 -0.0261 -0.0215 -0.0296 -0.0246 -0.0188 -0.0285 -0.0119 -0.0246 -0.0198 -0.0266 -0.0117 0.0228 0.037 -0.0022 0.0181 -0.00114 -0.0155 -0.00442 -0.00161 0.000777
Hernals 0.0421 0.122 -0.166 -0.0104 -0.0143 0.00604 -0.0505 -0.00838 -0.0349 -0.0285 -0.00557 -0.0454 -0.0347 -0.03 -0.0275 -0.041 -0.0194 1 -0.019 -0.0434 -0.0335 -0.0567 -0.0602 -0.0131 -0.045 -0.0418 -0.0344 -0.0474 -0.0394 -0.0301 -0.0457 -0.019 -0.0394 -0.0317 -0.0265 -0.0128 0.0288 0.00702 0.0087 -0.0198 -0.00183 0.0123 -0.00709 -0.00259 0.0216
Hietzing 0.00891 -0.13 -0.254 -0.00706 0.00642 -0.00218 0.0136 0.0259 -0.027 -0.00832 -0.0304 -0.0278 -0.0213 -0.0184 -0.0169 -0.0251 -0.0119 -0.019 1 -0.0266 -0.0205 -0.0347 -0.0369 -0.00802 -0.0275 -0.0256 -0.0211 -0.029 -0.0241 -0.0184 -0.028 -0.0117 -0.0241 -0.0194 0.00642 -0.0019 -0.0043 -0.00927 0.00255 0.0109 -0.00112 -0.0128 -0.00434 -0.00158 -0.00694
Innere Stadt -0.0242 0.041 0.0811 0.104 0.0805 0.0592 0.272 0.0194 0.0823 -0.00213 0.0418 -0.0634 -0.0485 -0.0419 -0.0384 -0.0572 -0.0271 -0.0434 -0.0266 1 -0.0467 -0.0791 -0.0841 -0.0183 -0.0627 -0.0584 -0.048 -0.0661 -0.055 -0.042 -0.0638 -0.0266 -0.0549 -0.0442 0.0764 0.04 -0.0857 -0.0102 -0.0634 -0.0247 -0.00255 0.0837 0.0132 -0.00361 0.0658
Josefstadt -0.0371 0.0483 -0.0716 -0.0375 -0.0212 -0.0398 -0.0183 -0.041 -0.00349 0.00605 -0.0229 -0.0489 -0.0374 -0.0323 -0.0296 -0.0442 -0.0209 -0.0335 -0.0205 -0.0467 1 -0.0611 -0.0649 -0.0141 -0.0484 -0.045 -0.0371 -0.051 -0.0424 -0.0324 -0.0492 -0.0205 -0.0424 -0.0341 -0.0392 -0.0202 0.0467 -0.00941 0.018 -0.00768 -0.00197 -0.00857 -0.00764 -0.00279 -0.0406
Landstraße -0.00956 -0.0983 0.308 0.0262 0.00622 -0.0138 0.0048 -0.0247 0.0563 0.02 0.0548 -0.0829 -0.0634 -0.0548 -0.0502 -0.0749 -0.0354 -0.0567 -0.0347 -0.0791 -0.0611 1 -0.11 -0.0239 -0.082 -0.0763 -0.0628 -0.0865 -0.0719 -0.0549 -0.0834 -0.0347 -0.0718 -0.0579 0.0272 -0.0202 -0.0218 -0.00597 -0.0156 0.00997 -0.00334 0.00612 -0.0129 -0.00472 -0.00638
Leopoldstadt -0.00725 0.237 0.293 -0.0306 -0.0312 -0.00811 -0.0339 -0.0163 0.0145 -0.031 0.0201 -0.088 -0.0673 -0.0582 -0.0534 -0.0795 -0.0376 -0.0602 -0.0369 -0.0841 -0.0649 -0.11 1 -0.0254 -0.0871 -0.0811 -0.0667 -0.0919 -0.0764 -0.0583 -0.0886 -0.0369 -0.0763 -0.0615 0.0188 -0.033 -0.012 0.00367 0.00622 -0.00255 -0.00355 -0.00342 -0.00498 0.019 0.00622
Liesing 0.0303 -0.242 -0.139 0.00194 0.0342 0.00695 -0.00612 0.0445 -0.0194 -0.00169 0.0034 -0.0191 -0.0146 -0.0127 -0.0116 -0.0173 -0.00817 -0.0131 -0.00802 -0.0183 -0.0141 -0.0239 -0.0254 1 -0.0189 -0.0176 -0.0145 -0.02 -0.0166 -0.0127 -0.0193 -0.00802 -0.0166 -0.0134 0.000743 -0.00791 0.00238 -0.00638 -0.00549 0.0273 -0.000771 -0.0218 -0.00299 -0.00109 0.00923
Margareten -0.0343 -0.231 -0.0362 -0.00598 -0.00824 0.0015 -0.0534 -0.035 -0.0132 -0.0184 -0.0264 -0.0657 -0.0502 -0.0434 -0.0398 -0.0593 -0.028 -0.045 -0.0275 -0.0627 -0.0484 -0.082 -0.0871 -0.0189 1 -0.0605 -0.0498 -0.0686 -0.057 -0.0435 -0.0661 -0.0275 -0.0569 -0.0459 -0.00783 -0.0186 0.0147 -0.0113 0.0283 0.0023 -0.00265 -0.028 -0.0103 -0.00374 -0.0493
Mariahilf -0.0257 -0.132 -0.0746 -0.0216 -0.00223 -0.0148 0.00505 -0.0255 -0.000285 0.0169 -0.01 -0.0611 -0.0467 -0.0404 -0.037 -0.0552 -0.0261 -0.0418 -0.0256 -0.0584 -0.045 -0.0763 -0.0811 -0.0176 -0.0605 1 -0.0463 -0.0638 -0.053 -0.0405 -0.0615 -0.0256 -0.053 -0.0427 -0.000961 -0.00247 0.00335 -0.00913 0.000277 -0.0031 -0.00246 0.00379 -0.00954 -0.00348 -0.0118
Meidling 0.0519 -0.264 -0.17 -0.0241 -0.0218 -0.0153 -0.0141 0.0215 -0.0259 -0.00981 0.0245 -0.0503 -0.0385 -0.0333 -0.0305 -0.0454 -0.0215 -0.0344 -0.0211 -0.048 -0.0371 -0.0628 -0.0667 -0.0145 -0.0498 -0.0463 1 -0.0525 -0.0436 -0.0333 -0.0506 -0.0211 -0.0436 -0.0351 0.004 -0.00445 -0.00629 0.0168 0.0122 0.0112 -0.00203 -0.0232 0.00641 -0.00287 0.0205
Neubau -0.0514 -0.037 -0.114 -0.047 -0.0373 -0.0387 -0.00681 -0.076 0.0485 0.0194 -0.0132 -0.0693 -0.053 -0.0458 -0.042 -0.0626 -0.0296 -0.0474 -0.029 -0.0661 -0.051 -0.0865 -0.0919 -0.02 -0.0686 -0.0638 -0.0525 1 -0.0601 -0.0459 -0.0697 -0.029 -0.06 -0.0484 -0.0348 0.0449 0.0273 -0.013 -0.00712 0.000513 -0.00279 0.00462 0.0213 -0.00395 -0.0412
Ottakring 0.0357 0.0631 -0.233 -0.0178 -0.000396 -0.0104 -0.0331 -0.000715 -0.0167 0.000713 0.0159 -0.0576 -0.044 -0.0381 -0.0349 -0.052 -0.0246 -0.0394 -0.0241 -0.055 -0.0424 -0.0719 -0.0764 -0.0166 -0.057 -0.053 -0.0436 -0.0601 1 -0.0382 -0.058 -0.0241 -0.0499 -0.0402 -0.00754 -0.00937 0.0114 -0.00733 0.0107 -0.0046 -0.00232 -0.00459 -0.00899 -0.00328 -0.0166
Penzing 0.0313 -0.0748 -0.345 -0.0159 -0.011 0.000331 -0.046 0.0545 -0.039 0.00362 -0.0179 -0.044 -0.0336 -0.0291 -0.0267 -0.0397 -0.0188 -0.0301 -0.0184 -0.042 -0.0324 -0.0549 -0.0583 -0.0127 -0.0435 -0.0405 -0.0333 -0.0459 -0.0382 1 -0.0443 -0.0184 -0.0381 -0.0307 -0.0226 -0.012 0.0244 0.00818 0.0074 0.00403 0.0608 -0.0117 -0.00687 -0.00251 -0.00601
Rudolfsheim-Fünfhaus 0.00962 -0.157 -0.241 0.000718 -0.02 0.00217 -0.0322 0.0273 0.00155 -0.00839 -0.00748 -0.0668 -0.0511 -0.0442 -0.0405 -0.0603 -0.0285 -0.0457 -0.028 -0.0638 -0.0492 -0.0834 -0.0886 -0.0193 -0.0661 -0.0615 -0.0506 -0.0697 -0.058 -0.0443 1 -0.028 -0.0579 -0.0466 0.00416 0.044 -0.0145 -0.00147 0.00715 0.00285 -0.00269 -0.00856 -0.0104 -0.00381 0.0201
Simmering -0.00106 -0.176 0.185 -0.0133 -0.0128 0.0129 -0.00957 0.0254 -0.0286 -0.0373 -0.0128 -0.0278 -0.0213 -0.0184 -0.0169 -0.0251 -0.0119 -0.019 -0.0117 -0.0266 -0.0205 -0.0347 -0.0369 -0.00802 -0.0275 -0.0256 -0.0211 -0.029 -0.0241 -0.0184 -0.028 1 -0.0241 -0.0194 -0.0228 -0.0115 0.021 0.0262 0.0115 0.0256 -0.00112 -0.036 -0.00434 -0.00158 -0.0313
Wieden -0.0218 -0.147 0.0593 0.00765 0.0384 0.00235 0.0187 -0.024 0.0545 0.0165 0.0251 -0.0575 -0.044 -0.038 -0.0349 -0.052 -0.0246 -0.0394 -0.0241 -0.0549 -0.0424 -0.0718 -0.0763 -0.0166 -0.0569 -0.053 -0.0436 -0.06 -0.0499 -0.0381 -0.0579 -0.0241 1 -0.0402 0.0226 -0.0238 -0.0183 0.00458 -0.0125 -0.0169 -0.00232 0.0275 0.0162 -0.00328 0.00732
Währing 0.00136 0.192 -0.121 -0.0442 -0.0221 -0.0342 -0.0207 -0.0148 -0.04 0.0123 -0.0486 -0.0463 -0.0354 -0.0306 -0.0281 -0.0418 -0.0198 -0.0317 -0.0194 -0.0442 -0.0341 -0.0579 -0.0615 -0.0134 -0.0459 -0.0427 -0.0351 -0.0484 -0.0402 -0.0307 -0.0466 -0.0194 -0.0402 1 -0.0273 -0.0191 0.0327 -0.000948 0.0233 -0.0115 -0.00187 -0.0111 -0.00723 0.0395 -0.016
Entire home/apt -0.0445 -0.0186 0.034 0.415 0.269 0.277 0.336 0.118 0.0882 0.0255 0.069 -0.0315 0.0517 -0.00856 -0.0336 0.0164 -0.0266 -0.0265 0.00642 0.0764 -0.0392 0.0272 0.0188 0.000743 -0.00783 -0.000961 0.004 -0.0348 -0.00754 -0.0226 0.00416 -0.0228 0.0226 -0.0273 1 -0.171 -0.954 -0.138 -0.221 0.063 -0.0167 0.145 0.025 -0.00723 0.0901
Hotel room 0.0188 0.00288 -0.0107 0.0232 0.0051 0.0222 0.0775 0.0803 -0.0139 -0.0375 -0.0147 0.0615 -0.021 0.00659 -0.00319 0.00305 -0.0117 -0.0128 -0.0019 0.04 -0.0202 -0.0202 -0.033 -0.00791 -0.0186 -0.00247 -0.00445 0.0449 -0.00937 -0.012 0.044 -0.0115 -0.0238 -0.0191 -0.171 1 -0.0632 -0.00914 -0.0203 -0.0488 -0.0011 0.0683 -0.00428 -0.00156 0.095
Private room 0.0389 0.016 -0.0349 -0.426 -0.267 -0.301 -0.353 -0.149 -0.0841 -0.0135 -0.064 0.0187 -0.0458 0.00705 0.0281 -0.0181 0.0228 0.0288 -0.0043 -0.0857 0.0467 -0.0218 -0.012 0.00238 0.0147 0.00335 -0.00629 0.0273 0.0114 0.0244 -0.0145 0.021 -0.0183 0.0327 -0.954 -0.0632 1 -0.051 0.22 -0.0496 -0.00616 -0.157 -0.0239 0.00801 -0.111
Shared room 0.0104 0.0115 0.014 -0.0181 -0.0442 0.0613 -0.0484 0.0502 -0.0136 -0.0186 -0.0154 -0.00644 -0.0103 0.000623 0.0363 0.0029 0.037 0.00702 -0.00927 -0.0102 -0.00941 -0.00597 0.00367 -0.00638 -0.0113 -0.00913 0.0168 -0.013 -0.00733 0.00818 -0.00147 0.0262 0.00458 -0.000948 -0.138 -0.00914 -0.051 1 0.0541 -0.016 0.121 -0.0375 -0.00345 -0.00126 -0.0206
flexible 0.153 0.00575 -0.0182 -0.127 -0.07 -0.086 -0.155 -0.0845 -0.162 -0.0375 -0.0769 0.0111 -0.0226 0.0107 0.00666 -0.0157 -0.0022 0.0087 0.00255 -0.0634 0.018 -0.0156 0.00622 -0.00549 0.0283 0.000277 0.0122 -0.00712 0.0107 0.0074 0.00715 0.0115 -0.0125 0.0233 -0.221 -0.0203 0.22 0.0541 1 -0.471 -0.00661 -0.473 -0.0256 -0.00934 -0.0164
moderate -0.0798 -0.0194 0.0069 -0.00484 -0.00328 -0.0155 -0.0391 -0.00736 0.04 0.0713 0.00482 -0.0256 0.011 0.00452 0.0234 0.0156 0.0181 -0.0198 0.0109 -0.0247 -0.00768 0.00997 -0.00255 0.0273 0.0023 -0.0031 0.0112 0.000513 -0.0046 0.00403 0.00285 0.0256 -0.0169 -0.0115 0.063 -0.0488 -0.0496 -0.016 -0.471 1 -0.00768 -0.55 -0.0298 -0.0109 -0.0709
strict 0.0167 -0.00574 -0.0196 -0.00832 -0.00534 -0.000275 -0.00623 0.0145 -0.00603 0.0023 0.0152 -0.00267 -0.00204 -0.00177 -0.00162 -0.00241 -0.00114 -0.00183 -0.00112 -0.00255 -0.00197 -0.00334 -0.00355 -0.000771 -0.00265 -0.00246 -0.00203 -0.00279 -0.00232 0.0608 -0.00269 -0.00112 -0.00232 -0.00187 -0.0167 -0.0011 -0.00616 0.121 -0.00661 -0.00768 1 -0.00772 -0.000417 -0.000152 0.00946
strict_14_with_grace_period -0.067 0.0131 0.0102 0.123 0.069 0.0946 0.183 0.0846 0.116 -0.0333 0.0708 0.0151 0.00649 -0.0139 -0.029 -0.000818 -0.0155 0.0123 -0.0128 0.0837 -0.00857 0.00612 -0.00342 -0.0218 -0.028 0.00379 -0.0232 0.00462 -0.00459 -0.0117 -0.00856 -0.036 0.0275 -0.0111 0.145 0.0683 -0.157 -0.0375 -0.473 -0.55 -0.00772 1 -0.0299 -0.0109 0.0826
super_strict_30 0.0165 0.00645 0.00842 0.0299 0.0107 0.0263 0.036 0.0288 -0.0249 -0.0288 -0.0336 0.000737 0.0486 -0.00685 -0.00628 0.00279 -0.00442 -0.00709 -0.00434 0.0132 -0.00764 -0.0129 -0.00498 -0.00299 -0.0103 -0.00954 0.00641 0.0213 -0.00899 -0.00687 -0.0104 -0.00434 0.0162 -0.00723 0.025 -0.00428 -0.0239 -0.00345 -0.0256 -0.0298 -0.000417 -0.0299 1 -0.00059 0.0367
super_strict_60 0.0105 0.0152 -0.0051 -0.00421 -0.00756 -0.000389 0.00105 -0.00085 -0.009 -0.00411 -0.0118 -0.00378 -0.00289 -0.0025 -0.00229 -0.00342 -0.00161 -0.00259 -0.00158 -0.00361 -0.00279 -0.00472 0.019 -0.00109 -0.00374 -0.00348 -0.00287 -0.00395 -0.00328 -0.00251 -0.00381 -0.00158 -0.00328 0.0395 -0.00723 -0.00156 0.00801 -0.00126 -0.00934 -0.0109 -0.000152 -0.0109 -0.00059 1 0.0134
instant_t 0.239 0.00538 0.0226 0.158 0.0568 0.106 0.119 0.14 0.151 -0.0964 0.311 0.0093 0.0154 0.0143 -0.0166 0.0387 0.000777 0.0216 -0.00694 0.0658 -0.0406 -0.00638 0.00622 0.00923 -0.0493 -0.0118 0.0205 -0.0412 -0.0166 -0.00601 0.0201 -0.0313 0.00732 -0.016 0.0901 0.095 -0.111 -0.0206 -0.0164 -0.0709 0.00946 0.0826 0.0367 0.0134 1

Tarkastelin vielä tarkemmin piirteiden korrelointia hinnan kanssa saadakseni perusteita hinnan ennustemallille. Poimin kaikki itseisarvoltaan 0.1 suuremmat korrelaatiot hinnan kanssa ja tulostin ne suuruusjärjestyksessä. Joukosta erottui muutama selvästi nollasta poikkeava korrelaatio. Selvästi suurin negatiivoinen korrelaatio on piirteen 'Private room' kanssa ja selvimmät positiiviset korrelaatiot ovat piirteiden 'accommodates', 'bedrooms', 'beds', 'Entire home/apt', 'Innere Stadt' ja 'availability_30' kanssa, joista 'Entire home/apt' kuvaa selvästi samaa asiaa kuin 'Private room' mutta vastakkaiseen suuntaan. Kaupunginosista ainoastaan Innere Stadt näyttäisi selvästi vaikuttavan hintaan, mikä on tullut esiin jo aiemmissakin tarkasteluissa.

In [44]:
corr.price.sort_values(ascending=False)[abs(corr.price) > 0.1]
Out[44]:
price                          1.000000
accommodates                   0.514534
bedrooms                       0.458159
beds                           0.405741
Entire home/apt                0.336088
Innere Stadt                   0.272136
availability_30                0.229649
strict_14_with_grace_period    0.182625
instant_t                      0.118858
flexible                      -0.154881
Private room                  -0.352623
Name: price, dtype: float64

Pandaksen funktiolla scatter_matrix puolestaan saadaan tehtyä hajontapistematriisi, jonka avulla voi tarkastella piirteiden välisiä keskinäisiä riippuvuuksia. Voidaan esimerkiksi tarkastella, esiintyykö piirteiden välillä kollineaarisuutta tai selittävillä piirteillä heteroskedastisuutta (KvantiMOTV/Regressioanalyysin rajoitteet). Lävistäjämatriisilla näkyy puolestaan kunkin piirteen arvoalue histogrammeina. Selittävien piirteiden välinen kollineaarisuus vaikuttaisi negatiivisesti regressiomallin tuloksiin, ja todennäköisesti se ilmenisi hajantapistematriisiin muodostuvina suorina.

Alla olevasta hajontapistematriisista nähdään, ettei kollineaarisuuden suhteen esiinny selviä ongelmia valitussa selittäjäjoukossa. Sänkyjen sekä makuuhuoneiden lukumäärän ja majoituskapasiteetin välillä on luonnollisesti jonkin asteinen riippuvuus, mutta niidenkään välinen pistejoukko ei muodosta selvää suoraa, joten riippuvuus ei ole liian vahva. Erilaiset majoitusratkaisut eri huoneistoissa vaikuttavanet siihen, ettei suoraa riippuvuutta ole.

Heteroskedastisuus näkyisi hajontapistematriisissa selitettävän (hinta) ja selittävien muuttujien (muut) hajontakuvioissa suorina, joissa virhetermin arvo vaihtelisi suuresti selittäjän arvon mukaan. Heteroskedastisuus ei estä piirteen käyttämistä selittäjänä, mutta vaikuttaa siihen, mitä menetelmää kannattaa käyttää. Sitäkään ei ole selvästi havaittavissa valituissa piirteissä. Tosin millään matriisin selittävillä piirteellä ja hinnalla ei näyttäsi olevan myöskään selvää lineaarista riippuvuutta.

In [45]:
scattercols = ['price', 'accommodates', 'number_of_reviews', 'reviews_per_month',
               'beds', 'bedrooms', 'availability_30', 'review_scores_rating']
axs = pd.plotting.scatter_matrix(alldata[scattercols], figsize=(15, 15), c='red')

5. Koneoppiminen

Nyt kun data on jalostettu sopivaan muotoon ja sen sisältöä ymmärretään riittävästi, voidaan luoda asunnon hinnan ennustemalli tai -malleja. Kokeillaan ensin käyttää hinnan ennustamiseen alkuperäisen esimerkin piirrejoukkoa. Kirjaston scikit-learn avulla on helppo kokeilla useita erilaisia lineaarisia ennustemalleja samalla datalla. Kokeillaan aluksi kuutta eri mallia: pienimmän neliösumman lineaarinen regressio, Ridge-regressio, Lasso-regressio, ElasticNet-regressio, Bayesian-regressio ja Orthogonal Matching Pursuit (OMP). (https://scikit-learn.org/stable/modules/linear_model.html)

Vertaillaan mallien hyvyyttä käyttäen mittarina virhetermien itseisarvon mediaania. Virheiden mediaani on esimerkiksi keskimääräistä neliövirhettä parempi mittari, koska outlierit vaikuttavat siihen kohtuullisen vähän. Selkeimmät outlierit toki suodatin jo pois dataa siivotessani, mutta aineistoon jäi kuitenkin merkittävästi keskiarvoa ja mediaania suurempia arvoja. Virheiden mediaani on hyvä mittari myös siksi että sen yksikkö on sama testattavan suureen (tässä hinnan) kanssa ja arvot ovat siten helposti ymmärrettäviä.

Piirrematriisista poistetaan ennustettava piirre (hinta) sekä siihen visualisointien vuoksi lisätyt piirteet, joita ei ollut tarkoituskaan käyttää ennustamisessa. Tämän jälkeen piirrematriisi ja niitä vastaavat hinnat jaetaan kahteen osaan funktiolla train_test_split. Testiä varten valitaan arvoista 20% satunnaisesti. Kaikki mallit opetetaan niiden metodeilla fit ja tämän jälkeen niiden testiaineistosta muodostamat ennusteet (funktion predict palauttama vektori) testataan testiaineiston todellisia hintoja vastaan ja virhe tulostetaan. Lopuksi piirretään eri mallien virheistä pylväsdiagrammit. Kaikki mallit tuottavat suunnilleen samansuuruisen virheen (virhealue on 17.01 - 18.88); vain ElasticNet-mallin virheen voi katsoa olevan vähän muita suurempi. Tulosten perusteella valitsisin perinteisen pienimmän neliösumman regression, koska ymmärrän malleista kunnolla vain sen toiminnan ja sen virhe on vain puoli prosenttia pienintä virhettä suurempi. Mitään todellista hyvyyseroa ei siis ole. Katsotaan lopuksi, miltä ennustukset näyttävät suhteessa todellisiin arvoihin näillä menetelmällä. Suhteelliset poikkeamat ovat kohtuullisen suuria eikä ennustin vaikuta kovin käyttökelpoiselta.

In [46]:
rs = 1
ests = [ linear_model.LinearRegression(), linear_model.Ridge(),
         linear_model.Lasso(), linear_model.ElasticNet(),
         linear_model.BayesianRidge(), linear_model.OrthogonalMatchingPursuit() ]
ests_labels = np.array(['Linear', 'Ridge', 'Lasso', 'ElasticNet', 'BayesRidge', 'OMP'])
errvals = np.array([])

X_train, X_test, y_train, y_test = \
    train_test_split(alldata.drop(['price', 'id', 'listing_url', 'longitude', 'latitude', 
                                   'BezirkeNbr', 'Zip'], axis=1),
                     alldata.price, test_size=0.2, random_state=20)

for e in ests:
    e.fit(X_train, y_train)
    this_err = metrics.median_absolute_error(y_test, e.predict(X_test))
    print("got error %0.2f" % this_err)
    errvals = np.append(errvals, this_err)

pos = np.arange(errvals.shape[0])
srt = np.argsort(errvals)
plt.figure(figsize=(7,5))
plt.bar(pos, errvals[srt], align='center')
plt.xticks(pos, ests_labels[srt])
plt.xlabel('Estimator')
plt.ylabel('Median Absolute Error')
got error 17.11
got error 17.13
got error 17.01
got error 18.88
got error 17.02
got error 17.69
Out[46]:
Text(0, 0.5, 'Median Absolute Error')
In [47]:
pd.DataFrame({'Price' : y_test, ests_labels[0] : ests[0].predict(X_test),
                                ests_labels[1] : ests[1].predict(X_test),
                                ests_labels[2] : ests[2].predict(X_test),
                                ests_labels[3] : ests[3].predict(X_test),
                                ests_labels[4] : ests[4].predict(X_test),              
                                ests_labels[5] : ests[5].predict(X_test),
             })
Out[47]:
Price Linear Ridge Lasso ElasticNet BayesRidge OMP
3409 50.0 107.450597 107.496787 121.339800 115.409047 108.082469 88.569778
9433 110.0 96.103514 96.332259 103.983354 97.500745 98.456892 90.038221
7194 160.0 116.847854 116.854666 117.203757 117.589106 116.900422 113.672414
4095 100.0 67.516836 67.540983 76.766661 80.406772 67.827682 86.805186
11122 79.0 89.046189 89.023173 79.117653 77.458759 88.748124 59.690296
... ... ... ... ... ... ... ...
4305 290.0 359.702246 359.678286 354.256286 321.223250 359.401467 258.905681
1219 75.0 86.469635 86.478496 80.802332 78.735357 86.574903 89.436595
2705 45.0 61.263744 61.260206 57.676449 64.664370 61.214116 33.506692
1759 30.0 15.494184 15.571982 35.166253 42.585889 16.310916 38.862383
6427 35.0 45.050029 45.063963 57.412551 58.312141 45.274078 71.870121

1858 rows × 7 columns

Seuraavaksi kokeillaan hyperparametrien optimointia (hyperparameter tunining) ja katsotaan, saadaanko sen avulla parempi tulos. Hyperparametreilla tarkoitetaan sellaisia mallin parametreja, joiden arvo ei muutu koneoppimisen opetusprosessin aikana. Hyperparametrien optimointi löytää hyperparametrien joukon, joka tuottaa optimaalisen mallin ja joka minimoi ennalta määritellyn häviöfunktion käytetyllä syötedatalla. Yleinen optimoinnissa käytettyy menetelmä on grid search, joka kokeilee kaikkia valittujen parametrien arvokombinaatioita ja käyttää ristiinvalidointia löytääkseen parhaan kombinaation. Kirjaston sklearn funktiolla GridSearchCV voidaan tehdä grid search valitun menetelmän valituille parametreille.

Valitaan optimoitavaksi ennustusmenetelmäksi GradientBoostingRegressor ja ristiinvalidoinniksi 3-fold. Määritetään menetelmän optimoitavat parametrit ja niille kokeiltavat arvot sekä validoinnissa käytettävä hyvyysfunktio. Minimoitavaksi häviöfunktioksi valitaan virhetermien itseisarvojen mediaani, mutta koska funktio GridSearchCV maksimoi mittariaan (score), pitää mittariksi valita virhetermien itseisarvojen mediaanin vastaluku (neg_median_absolute_error). Optmoitavat parametrit välitetään GridSearchCV-funktiolle sanakirjamuotoisella parametrilla (tuned_parameters). Kun optimointi on tehty, katsotaan, millaisen GradientBoostingRegressor-menetelmän GridSearchCV-funktio löysi. Funktion GridSearchCV suorittama tyhjentävä haku ja ristiinvalidointi kuluttavat erittäin paljon CPU-aika, jos optimoitaville parametreille kokeiltavia kombinaatioita on paljon, ja siksi tässä esimerkissä optimoitaville parametreille annetaan todella vähän kokeiltavia arvoja. Katsotaan lopuksi löydetyn menetelmän virhe ja miltä ennustukset näyttävät suhteessa todellisiin arvoihin myös tällä menetelmällä. Voidaan todeta, että ainakin mediaanilla mitattuna optimoitu GradientBoostingRegressor-menetelmä tuotti huomattavasti edellä kokeiluja menetelmiä paremman tuloksen, vaikka optimointia ei käytännössä tehty. Todennäköisesti menetelmän hyperparametreihin oli osattu antaa hyvät sivistyneet arvaukset.

In [48]:
import sklearn
print('The scikit-learn version is {}.'.format(sklearn.__version__))

#sorted(sklearn.metrics.SCORERS.keys())
The scikit-learn version is 0.21.3.
In [49]:
n_est = 500

tuned_parameters = {
    "n_estimators": [ n_est ],
    "max_depth" : [ 4 ],
    "learning_rate": [ 0.01 ],
    "min_samples_split" : [ 2 ],
    "loss" : [ 'ls', 'lad' ]
}

gbr = ensemble.GradientBoostingRegressor()
clf = GridSearchCV(gbr, cv=3, param_grid=tuned_parameters,
        scoring='neg_median_absolute_error')
preds = clf.fit(X_train, y_train)
best_gbr = clf.best_estimator_
best_gbr
Out[49]:
GradientBoostingRegressor(alpha=0.9, criterion='friedman_mse', init=None,
                          learning_rate=0.01, loss='lad', max_depth=4,
                          max_features=None, max_leaf_nodes=None,
                          min_impurity_decrease=0.0, min_impurity_split=None,
                          min_samples_leaf=1, min_samples_split=2,
                          min_weight_fraction_leaf=0.0, n_estimators=500,
                          n_iter_no_change=None, presort='auto',
                          random_state=None, subsample=1.0, tol=0.0001,
                          validation_fraction=0.1, verbose=0, warm_start=False)
In [50]:
abs(clf.best_score_)
Out[50]:
12.401737522700905
In [51]:
pd.DataFrame({'Price' : y_test, 'Best GBR' : best_gbr.predict(X_test)})
Out[51]:
Price Best GBR
3409 50.0 95.022344
9433 110.0 84.090457
7194 160.0 92.416657
4095 100.0 65.034795
11122 79.0 76.321884
... ... ...
4305 290.0 164.545184
1219 75.0 66.658912
2705 45.0 40.155251
1759 30.0 28.736211
6427 35.0 48.407491

1858 rows × 2 columns

Tarkastellaan vielä, kuinka jokainen boosting-kierros vaikuttaa menetelmän virheeseen, jotta nähdään, kannattaisiko iteraatioiden määrää kasvattaa GradientBoostingRegressor-menetelmässä. Optimoinnin valitsemassa menetelmässä oli käytössä tappiofunktiona pienin absoluuttinen poikkeama (LAD), joka minimoi virhetermien itseisarvojen summaa ja toimii tässä pienintä nelisummaa paremmin (jos uskotaan optimoinnin olevan oikeassa). Kuvasta näkyy, että poikkeama alkaa tasaantua 300-400 iteraation kohdalla, jonka jälkeen menetelmän sovitusvirhe ja siten myöskään ennustustulos ei enää merkittävästi parane. Tein sovituksen 500 iteraatiolla.

In [52]:
# plot error for each round of boosting
test_score = np.zeros(n_est, dtype=np.float64)
train_score = best_gbr.train_score_

for i, y_pred in enumerate(best_gbr.staged_predict(X_test)):
    test_score[i] = best_gbr.loss_(y_test, y_pred)
    
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.plot(np.arange(n_est), train_score, 'darkblue', label='Training Set Error')
plt.plot(np.arange(n_est), test_score, 'red', label='Test Set Error')
plt.legend(loc='upper right')
plt.xlabel('Boosting Iterations')
plt.ylabel('Least Absolute Deviation')
Out[52]:
Text(0, 0.5, 'Least Absolute Deviation')

Menetelmää voisi yrittää vielä parantaa määrittelemällä optimoitaville parametreille enemmän tutkittavia arvoja, mutta siihen tarvittaisiin paljon laskentakapasiteettia eli käytännössä laskenta pitäisi hajauttaa johonkin klusteriin. Esimerkissä ohjeistettiin käyttämään pakettia spark-sklearn optimoinnin hajauttamiseksi Spark-klusteriin.

Tarkastellaan lopuksi vielä eri muuttujien merkitystä löydetyssä mallissa vertailemalla niiden merkityksiä merkitykseltään suurimpaan muuttujaan. Alla olevasta kuvasta nähdään, että merkittävimmät muuttujat ovat 'Private room', 'accommodates', 'availability_30', 'Innere Stadt' ja 'bedrooms', joista merkittävin (Private room) vaikuttaa hintaan negatiivisesti ja muut positiivisesti. Vaikutusten suunta on helppo tarkastaa visualisoinnin yhteydessä lasketuista korrelaatioista, sillä jokaisella näistä muuttujista oli riittävän vahva korrelaatio hinnan kanssa tämän päättelemiseksi. Kuten aiemmin totesin yksityinen huone kuvaa muuttujana kohtuullisen suoraan kokonaisen huoneiston vastakohtaa, joten suurin hintaennuste on kokonaan käytössä olevilla asunnoilla, joissa on suuri majoituskapasiteetti ja monta makuuhuonetta, ja jotka ovat usein saatavilla ja sijaitsevat ydinkeskustassa - ei kovin yllättävä tulos.

In [53]:
feature_importance = clf.best_estimator_.feature_importances_
# make importances relative to max importance
feature_importance = 100.0 * (feature_importance / feature_importance.max())
sorted_idx = np.argsort(feature_importance)
sorted_idx = sorted_idx[sorted_idx.size-15:sorted_idx.size]
pos = np.arange(sorted_idx.shape[0]) + .5
pvals = feature_importance[sorted_idx]
pcols = X_train.columns[sorted_idx]

plt.figure(figsize=(8,10))
plt.barh(pos, pvals, align='center')
plt.yticks(pos, pcols)
plt.xlabel('Relative Importance')
plt.title('Variable Importance')
Out[53]:
Text(0.5, 1.0, 'Variable Importance')

Tein lopuksi vielä menetelmiä LinearRegression ja GradientBoostingRegressor käyttäen uudet koneoppimismallit, joihin otin mukaan vain nämä viisi edellä mainittua muuttujaa. Näiden mallien avulla voi harjoitustyön lopussa ennustaa kohteen hintaa käyttäjän antaman syötteen perusteella. Myös arvioiden määrä ja arviolukema vaikuttaa hintaan jonkun verran, mutta jätin nämä näistä uusista ennustemalleista, sillä kyseisiä tietoja ei ole saatavissa uusista kohteista. Ainakaan mediaanivirheet eivät kasvaneet merkittävästi, vaikka selittäjämuuttujien määrän tiputti viiteen.

In [54]:
short_feature_matrix = alldata[['Private room', 'accommodates', 'availability_30', 
                                'Innere Stadt', 'bedrooms']]
model_linear = linear_model.LinearRegression()
F_train, F_test, p_train, p_test = train_test_split(short_feature_matrix, alldata.price,
                                                    test_size=0.2, random_state=20)

model_linear.fit(F_train, p_train)
print("Kertoimet: ", model_linear.coef_)

metrics.median_absolute_error(y_test, e.predict(X_test))
Kertoimet:  [-22.67217659   8.58553859   0.82546229  59.02993619  18.27455509]
Out[54]:
17.692297983762042
In [55]:
preds = clf.fit(F_train, p_train)
model_gbr = clf.best_estimator_
model_gbr
Out[55]:
GradientBoostingRegressor(alpha=0.9, criterion='friedman_mse', init=None,
                          learning_rate=0.01, loss='lad', max_depth=4,
                          max_features=None, max_leaf_nodes=None,
                          min_impurity_decrease=0.0, min_impurity_split=None,
                          min_samples_leaf=1, min_samples_split=2,
                          min_weight_fraction_leaf=0.0, n_estimators=500,
                          n_iter_no_change=None, presort='auto',
                          random_state=None, subsample=1.0, tol=0.0001,
                          validation_fraction=0.1, verbose=0, warm_start=False)
In [56]:
abs(clf.best_score_)
Out[56]:
13.187007126651704
In [57]:
model_gbr.feature_importances_/model_gbr.feature_importances_.max()
Out[57]:
array([1.        , 0.22055893, 0.13801805, 0.12976243, 0.11824781])
In [58]:
pd.DataFrame({'Price' : p_test, 'Linear reg.' : model_linear.predict(F_test),
                                'Gradient boost' : model_gbr.predict(F_test)})
Out[58]:
Price Linear reg. Gradient boost
3409 50.0 128.260928 106.668793
9433 110.0 118.686297 84.431856
7194 160.0 118.077365 96.880281
4095 100.0 76.028035 66.365012
11122 79.0 70.413430 75.439944
... ... ... ...
4305 290.0 361.685664 150.930435
1219 75.0 69.424336 61.097854
2705 45.0 48.566715 38.605805
1759 30.0 29.581083 30.197114
6427 35.0 60.838798 52.888452

1858 rows × 3 columns

6. Toimeenpano

Tein siivotusta datasta Powor BI -ohjelmistolla vuorovaikutteisen dashboardin, jonka avulla vuokrauskohteita voi suodataa kaupunginosan, hinnan, majoituskapasiteetin, vuoteiden lukumäärän, makuuhuoneiden lukumäärän ja vuokrauskohteen tyypin perusteella. Kohteiden wep-sivulinkit ovat mukana, joten kohteiden tarkempiin tietoihin pääsee porautumaan suoraan kartasta klikkaamalla. Sivu avautuu uuteen välilehteen selaimessa. Dashboardin avulla matkailija voi hakea sopivaa majoituskohdetta eri kriteerien perusteella. Majoitusbisnestä pyörittävä voi puolestaan, tarkastella, mistä ja millaisen majoituskapasiteetin vuokrauskohteita kannattaa hankkia. Matkustajille oheispalveluita myyvä yrittäjä voi puolestaan tarkastella, missä olisi eniten hänen palveluidensa kohderyhmille sopivia vuokrauskohteita tarjolla. Kiinteistösijoittajan käyttöön aineiston data on mielestäni puutteellista, mutta omaa asuntoa etsivä voi käyttää dashboardia myös välttääkseen Airbnb-keskittymän tulevassa naapurustossaan. Jos Wienissä haluaisi pyörittää Airbnb-bisnestä, niin analyysin perusteella kannattaisi hankkia ydinkeskustasta majoituskapasiteetiltaan hyvä asunto ja vuokarata sitä yhtenä kokonaisuutena. Analyysi ei kyllä pysty mitenkään vertailemaan sellaisia tilanteita, joissa sama huoneisto vuokrattaisiin joko osina tai kokonaisuutena.

Jukan pitäisi päästä käyttämään dashboardia tuni-tunnuksellaan tuosta toimeenpanokohdan alussa olevasta linkistä. Kartat eivät aina kohdistu heti Wieniin, mutta sivun uudelleen lataamalla niiden pitäisi kohdistua oikein. Pikkukartalla tai pylväsdiagrammeista voi valita tarkasteltavan kaupunginosan, jolloin pääkartta zoomautuu siihen.

Foliumilla toteuttamiani vuorovaikutteisia karttavisualisointeja voi käyttää samoihin tarkoituksiin, mutta ne eivät ole käytettävyydeltään yhtä helppoja, sillä suodatukset joutuu tekemään aina koodilla dataframeen ja sen jälkeen joutuu generoimaan haluamansa kartan uudestaan. Toki koodatessa suodatuksille on vain mielikuvitus rajana. Myös Foliumilla toteutetussa kohdekartassa on kohteiden linkit mukana, joten siitäkin pääse suoraan tarkastelemaan kohteita. Kohteen sivu avautuu tästäkin kartasta uuteen välilehteen.

Lopuksi tein vielä pienen interaktiivisen sovelluksen, joka ennustaa viiden muuttujan yksinkertaistetuilla malleilla (model_linear ja model_gbr) Airbnb-kohteen hinnan Wienissä ja tulostaa nämä hinnat. Sovellus kysyy käyttäjältä yksitellen vuokrauskohteen arvot näille viidelle malleissa käytettävälle piirteelle.

In [59]:
def tolist_1_or_0(list, value):
    
    value = value.lower()
    if value in ['y', 'n']:
        list[0] = (value == 'y').real
        return True
    else:
        return False

def tolist_between(list, min, max, value):

    if (value >= min) and (value <= max):
        list[0] = value
        return True
    else:
        return False
    
In [60]:
def price_predictor():
    
    features = pd.DataFrame({'Private room' : [0], 'accommodates' : [0],
                             'availability_30' : [0], 'Innere Stadt' : [0], 'bedrooms' :[0]
                            })
    while True:
 
        print("To quit give something else asked!")

        try:
            
            if (tolist_1_or_0(features['Private room'], input('Private room (y/n): ')) and
                tolist_1_or_0(features['Innere Stadt'], input('Innere Stadt (y/n): ')) and
                tolist_between(features['accommodates'], 1, 20, 
                               int(input('Acommodates (1..20): '))) and
                tolist_between(features['bedrooms'], 1, 10,
                               int(input('Bedrooms (1..10): ')))    and
                tolist_between(features['availability_30'], 1, 30, 
                               int(input('Availability in a month (1..30): ')))
               ):
            
                print('\nPredicted price according gradient boosting regressor    :',
                      round(model_gbr.predict(features)[0], 1), '$'
                      '\nPredicted price according least square linear regression :', 
                      round(model_linear.predict(features)[0], 1),'$\n')            
            else:
                break
            
        except Exception:
            break
            
    print('Exit')
        
In [61]:
price_predictor()
To quit give something else asked!
Private room (y/n): y
Innere Stadt (y/n): y
Acommodates (1..20): 1
Bedrooms (1..10): 1
Availability in a month (1..30): 10

Predicted price according gradient boosting regressor    : 49.0 $
Predicted price according least square linear regression : 88.3 $

To quit give something else asked!
Private room (y/n): 
Exit